package com.noahsloan.nutils.xml.serial;

import java.io.File;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Node;

import com.noahsloan.nutils.ClassUtils;

/**
 * This class is safe for concurrent use by multiple threads. It is not a
 * singleton because it may be configured with different serializers.
 * <p>
 * 
 * @author noah
 * 
 */
public class DomSerializerManagerImpl implements DomSerializerManager {
	private Map<Class<?>, Serializer<?>> serializers = new HashMap<Class<?>, Serializer<?>>();

	private Map<Class<?>, DomSerializer<?>> domSerializers = new HashMap<Class<?>, DomSerializer<?>>();

	private DocumentBuilder builder;

	/**
	 * Creates a new instance with the default {@link DocumentBuilder}.
	 * 
	 * @see #DomSerializerManagerImpl()
	 * 
	 * @throws ParserConfigurationException
	 *             if a {@link DocumentBuilder} cannot be created.
	 */
	public DomSerializerManagerImpl() throws ParserConfigurationException {
		this(DocumentBuilderFactory.newInstance().newDocumentBuilder());
	}

	/**
	 * Creates a new instance with {@link Serializer}s for primitive types,
	 * {@link File}s, {@link Class}es, and {@link URL}s already registered.
	 * Additionally, an {@link AnnotatedObjectDomSerializer} is registered.
	 * 
	 */
	@SuppressWarnings("unchecked")
	public DomSerializerManagerImpl(DocumentBuilder builder) {
		AbstractSerializer[] known = { new BooleanSerializer(),
				new CharSequenceSerializer(), new ClassSerializer(),
				new FileSerializer(), new NumberSerializer(),
				new URLSerializer() };
		for (AbstractSerializer serializer : known) {
			register(serializer.getFromClass(), serializer);
		}
		register(Object.class, new AnnotatedObjectDomSerializer());
		this.builder = builder;
	}

	/**
	 * Registers a {@link Serializer} for the given class.
	 * 
	 * @param serializer
	 */
	public <T> void register(Class<T> clazz, Serializer<T> serializer) {
		serializers.put(clazz, serializer);
	}

	/**
	 * Register a {@link DomSerializer} for the given class.
	 * 
	 * @param <T>
	 * @param clazz
	 * @param serializer
	 */
	public <T> void register(Class<T> clazz, DomSerializer<T> serializer) {
		domSerializers.put(clazz, serializer);
	}

	/**
	 * 
	 * @param clazz
	 * @return a {@link Serializer} for clazz, a super type of clazz or an
	 *         interface of clazz.
	 */
	public Serializer<?> findSerializer(Class<?> clazz) {
		Map<Class<?>, Serializer<?>> map = serializers;
		return internalFind(clazz, map);
	}

	/**
	 * Looks for a mapping in map corresponding to clazz, a super class of
	 * clazz, or an interface of clazz, searched in that order. The first
	 * non-null value is returned.
	 * 
	 * @param <T>
	 * @param clazz
	 * @param map
	 * @return
	 */
	protected <T> T internalFind(Class<?> clazz, Map<Class<?>, T> map) {
		if (clazz.isPrimitive()) {
			clazz = ClassUtils.box(clazz);
		}
		Class<?>[] interfaces = clazz.getInterfaces();
		while (clazz != null) {
			T t = map.get(clazz);
			if (t == null) {
				clazz = clazz.getSuperclass();
			} else {
				return t;
			}
		}
		for (Class<?> _interface : interfaces) {
			T t = map.get(_interface);
			if (t != null) {
				return t;
			}
		}
		return null;
	}

	/**
	 * 
	 * @param obj
	 * @return obj converted to a {@link Document}
	 */
	public Document toDocument(Object obj) {
		Document doc = builder.newDocument();
		doc.appendChild(nextToDom(obj, doc));
		return doc;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.jsftoolkit.utils.serial.DomSerializerManager#nextToDom(java.lang.Object,
	 *      org.w3c.dom.Document)
	 */
	@SuppressWarnings("unchecked")
	public Node nextToDom(Object obj, Document doc) {
		if (obj == null) {
			return null;
		}
		Class<? extends Object> clazz = obj.getClass();
		Serializer serializer = findSerializer(clazz);
		if (serializer == null) {
			// can't get a NPE here because this class is a DomSerializer for
			// Object
			return findDomSerializer(clazz).toDom(obj, doc, this);
		} else {
			// if there is a serializer, create a text node
			return doc.createTextNode(serializer.serialize(obj));
		}
	}

	/**
	 * 
	 * @param clazz
	 * @return a {@link DomSerializer} for the given class or a superclass or
	 *         interface of that class.
	 */
	@SuppressWarnings("unchecked")
	public DomSerializer<Object> findDomSerializer(Class clazz) {
		return (DomSerializer<Object>) internalFind(clazz, domSerializers);
	}

	@SuppressWarnings("unchecked")
	public String serialize(Object obj) {
		if (obj == null) {
			return null;
		}
		Class attribClass = obj.getClass();
		Serializer serializer = findSerializer(attribClass);
		if (serializer == null) {
			throw new IllegalArgumentException(attribClass
					+ " does not have a Serializer!");
		}
		return serializer.serialize(obj);
	}
}
